<?php

/*
 * Copyright 2008 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#namespace tehframework\annotations;

#use \UnexpectedValueException;
#use \Reflector;
#use \ReflectionParameter;
#use tehframework\storage\Storage;
#use tehframework\annotations\parsers\DocBlockParser;
#use tehframework\annotations\parsers\state\StateParser;
#use tehframework\annotations\builders\DocBlockBuilder;

class DocBlockFactory
{
	/**
	 * @var Storage
	 */
	protected $storage;
	
	/**
	 * @var DocBlockParser
	 */
	protected $parser;
	
	/**
	 * @var DocBlockBuilder
	 */
	protected $builder;
	
	/**
	 * @param  Storage $storage
	 * @return DocBlockFactory
	 */
	public function setStorage(Storage $storage)
	{
		$this->storage = $storage;
		
		return $this;
	}
	
	/**
	 * @param  DocBlockParser $parser
	 * @return DocBlockFactory
	 */
	public function setParser(DocBlockParser $parser)
	{
		$this->parser = $parser;
		
		return $this;
	}
	
	/**
	 * @param  DocBlockBuilder $builder
	 * @return DocBlockFactory
	 */
	public function setBuilder(DocBlockBuilder $builder)
	{
		$this->builder = $builder;
		
		return $this;
	}
	
	/**
	 * @param  Reflector $target
	 * @return DocBlock
	 * @throws UnexpectedValueException
	 *         If the specified {@link $target Reflector} does not have
	 *         getDocComment() method defined.
	 * @throws BadMethodCallException
	 *         If no {@link $parser DocBlockParser} was specified prior to
	 *         calling this method.
	 * @throws BadMethodCallException
	 *         If no {@link $builder DocBlockBuilder} was specified prior to
	 *         calling this method.
	 * @throws ParserException
	 *         If the specified {@link $parser DocBlockParser} was not
	 *         able to parse the DocComment string.
	 * @throws BuilderException
	 *         If the specified {@link $builder DocBlockBuilder} was not
	 *         able to construct the DocBlock from its DocBlockDefinition.
	 */
	public function getDocBlock(Reflector $target)
	{
		$this->validateState($target instanceof ReflectionParameter
			? $target->getDeclaringFunction() : $target);
		
		$targetHash = $this->generateTargetHash($target);
		
		if ($targetHash === null)
		{
			return new DocBlock();
		}
		
		if ($this->storage &&
			(($docBlock = $this->storage->restore($targetHash)) !== null)
		)
		{
			return $docBlock;
		}
		
		$docBlock = $this->buildDocBlock($target);
		
		if ($this->storage)
		{
			$this->storage->store($targetHash, $docBlock);
		}
		
		return $docBlock;
	}
	
	/**
	 * @param  Reflector $target
	 * @throws UnexpectedValueException
	 * @throws BadMethodCallException
	 */
	protected function validateState(Reflector $target)
	{
		if (!method_exists($target, 'getDocComment'))
		{
			throw new UnexpectedValueException(sprintf(
				'[%s] does not have getDocComment() method defined.',
				get_class($target)));
		}
		
		if ($this->parser === null)
		{
			throw new BadMethodCallException('No parser specified.');
		}
		
		if ($this->builder === null)
		{
			throw new BadMethodCallException('No builder specified.');
		}
	}
	
	/**
	 * @param  Reflector $target
	 * @return string?
	 */
	protected function generateTargetHash(Reflector $target)
	{
		$docComment = $target instanceof ReflectionParameter
			? $target->getDeclaringFunction()->getDocComment()
			: $target->getDocComment();
		
		if (empty($docComment))
		{
			return null;
		}
		
		return hash(
			'md5', get_class($target) . $target->getName() . (string)$docComment
		);
	}
	
	/**
	 * @param  Reflector $target
	 * @return DocBlock
	 * @throws ParserException
	 * @throws BuilderException
	 */
	protected function buildDocBlock(Reflector $target)
	{
		$docComment = $target instanceof ReflectionParameter
			? $target->getDeclaringFunction()->getDocComment()
			: $target->getDocComment();
		
		$def = $this->parser->parseDocBlock($docComment);
		
		if ($target instanceof ReflectionParameter)
		{
			$this->buildParameterDefinition($target->getName(), $def);
		}
		
		if ($this->builder instanceof AnnotationTargetAware)
		{
			$this->builder->setAnnotationTarget($target);
		}
		
		return $this->builder->buildDocBlock($def);
	}
	
	/**
	 * @param  string $param
	 * @param  DocBlockDefinition $def
	 */
	protected function buildParameterDefinition(
		$param, DocBlockDefinition $def)
	{
		$def->shortDescription = null;
		$def->longDescription  = null;
		$def->tagDefinitions   = array();
		
		$annDefs = $def->annotationDefinitions;
		
		$def->annotationDefinitions = array();
		
		/* @var $annDef AnnotationDefinition */
		foreach ($annDefs as $annDef)
		{
			if ($annDef->parameter !== $param)
			{
				continue;
			}
			
			$annDef->parameter = null;
			
			$def->annotationDefinitions[] = $annDef;
		}
	}
}